export var MarkerCluster = L.MarkerCluster = L.Marker.extend({
    options: L.Icon.prototype.options,

    initialize: function (group, zoom, a, b) {

        L.Marker.prototype.initialize.call(this, a ? (a._cLatLng || a.getLatLng()) : new L.LatLng(0, 0),
            {icon: this, pane: group.options.clusterPane});

        this._group = group;
        this._zoom = zoom;

        this._markers = [];
        this._childClusters = [];
        this._childCount = 0;
        this._iconNeedsUpdate = true;
        this._boundsNeedUpdate = true;

        this._bounds = new L.LatLngBounds();

        if (a) {
            this._addChild(a);
        }
        if (b) {
            this._addChild(b);
        }
    },

    //Recursively retrieve all child markers of this cluster
    getAllChildMarkers: function (storageArray, ignoreDraggedMarker) {
        storageArray = storageArray || [];

        for (var i = this._childClusters.length - 1; i >= 0; i--) {
            this._childClusters[i].getAllChildMarkers(storageArray);
        }

        for (var j = this._markers.length - 1; j >= 0; j--) {
            if (ignoreDraggedMarker && this._markers[j].__dragStart) {
                continue;
            }
            storageArray.push(this._markers[j]);
        }

        return storageArray;
    },

    //Returns the count of how many child markers we have
    getChildCount: function () {
        return this._childCount;
    },

    //Zoom to the minimum of showing all of the child markers, or the extents of this cluster
    zoomToBounds: function (fitBoundsOptions) {
        var childClusters = this._childClusters.slice(),
            map = this._group._map,
            boundsZoom = map.getBoundsZoom(this._bounds),
            zoom = this._zoom + 1,
            mapZoom = map.getZoom(),
            i;

        //calculate how far we need to zoom down to see all of the markers
        while (childClusters.length > 0 && boundsZoom > zoom) {
            zoom++;
            var newClusters = [];
            for (i = 0; i < childClusters.length; i++) {
                newClusters = newClusters.concat(childClusters[i]._childClusters);
            }
            childClusters = newClusters;
        }

        if (boundsZoom > zoom) {
            this._group._map.setView(this._latlng, zoom);
        } else if (boundsZoom <= mapZoom) { //If fitBounds wouldn't zoom us down, zoom us down instead
            this._group._map.setView(this._latlng, mapZoom + 1);
        } else {
            this._group._map.fitBounds(this._bounds, fitBoundsOptions);
        }
    },

    getBounds: function () {
        var bounds = new L.LatLngBounds();
        bounds.extend(this._bounds);
        return bounds;
    },

    _updateIcon: function () {
        this._iconNeedsUpdate = true;
        if (this._icon) {
            this.setIcon(this);
        }
    },

    //Cludge for Icon, we pretend to be an icon for performance
    createIcon: function () {
        if (this._iconNeedsUpdate) {
            this._iconObj = this._group.options.iconCreateFunction(this);
            this._iconNeedsUpdate = false;
        }
        return this._iconObj.createIcon();
    },
    createShadow: function () {
        return this._iconObj.createShadow();
    },


    _addChild: function (new1, isNotificationFromChild) {

        this._iconNeedsUpdate = true;

        this._boundsNeedUpdate = true;
        this._setClusterCenter(new1);

        if (new1 instanceof L.MarkerCluster) {
            if (!isNotificationFromChild) {
                this._childClusters.push(new1);
                new1.__parent = this;
            }
            this._childCount += new1._childCount;
        } else {
            if (!isNotificationFromChild) {
                this._markers.push(new1);
            }
            this._childCount++;
        }

        if (this.__parent) {
            this.__parent._addChild(new1, true);
        }
    },

    /**
     * Makes sure the cluster center is set. If not, uses the child center if it is a cluster, or the marker position.
     * @param child L.MarkerCluster|L.Marker that will be used as cluster center if not defined yet.
     * @private
     */
    _setClusterCenter: function (child) {
        if (!this._cLatLng) {
            // when clustering, take position of the first point as the cluster center
            this._cLatLng = child._cLatLng || child._latlng;
        }
    },

    /**
     * Assigns impossible bounding values so that the next extend entirely determines the new bounds.
     * This method avoids having to trash the previous L.LatLngBounds object and to create a new one, which is much slower for this class.
     * As long as the bounds are not extended, most other methods would probably fail, as they would with bounds initialized but not extended.
     * @private
     */
    _resetBounds: function () {
        var bounds = this._bounds;

        if (bounds._southWest) {
            bounds._southWest.lat = Infinity;
            bounds._southWest.lng = Infinity;
        }
        if (bounds._northEast) {
            bounds._northEast.lat = -Infinity;
            bounds._northEast.lng = -Infinity;
        }
    },

    _recalculateBounds: function () {
        var markers = this._markers,
            childClusters = this._childClusters,
            latSum = 0,
            lngSum = 0,
            totalCount = this._childCount,
            i, child, childLatLng, childCount;

        // Case where all markers are removed from the map and we are left with just an empty _topClusterLevel.
        if (totalCount === 0) {
            return;
        }

        // Reset rather than creating a new object, for performance.
        this._resetBounds();

        // Child markers.
        for (i = 0; i < markers.length; i++) {
            childLatLng = markers[i]._latlng;

            this._bounds.extend(childLatLng);

            latSum += childLatLng.lat;
            lngSum += childLatLng.lng;
        }

        // Child clusters.
        for (i = 0; i < childClusters.length; i++) {
            child = childClusters[i];

            // Re-compute child bounds and weighted position first if necessary.
            if (child._boundsNeedUpdate) {
                child._recalculateBounds();
            }

            this._bounds.extend(child._bounds);

            childLatLng = child._wLatLng;
            childCount = child._childCount;

            latSum += childLatLng.lat * childCount;
            lngSum += childLatLng.lng * childCount;
        }

        this._latlng = this._wLatLng = new L.LatLng(latSum / totalCount, lngSum / totalCount);

        // Reset dirty flag.
        this._boundsNeedUpdate = false;
    },

    //Set our markers position as given and add it to the map
    _addToMap: function (startPos) {
        if (startPos) {
            this._backupLatlng = this._latlng;
            this.setLatLng(startPos);
        }
        this._group._featureGroup.addLayer(this);
    },

    _recursivelyAnimateChildrenIn: function (bounds, center, maxZoom) {
        this._recursively(bounds, this._group._map.getMinZoom(), maxZoom - 1,
            function (c) {
                var markers = c._markers,
                    i, m;
                for (i = markers.length - 1; i >= 0; i--) {
                    m = markers[i];

                    //Only do it if the icon is still on the map
                    if (m._icon) {
                        m._setPos(center);
                        m.clusterHide();
                    }
                }
            },
            function (c) {
                var childClusters = c._childClusters,
                    j, cm;
                for (j = childClusters.length - 1; j >= 0; j--) {
                    cm = childClusters[j];
                    if (cm._icon) {
                        cm._setPos(center);
                        cm.clusterHide();
                    }
                }
            }
        );
    },

    _recursivelyAnimateChildrenInAndAddSelfToMap: function (bounds, mapMinZoom, previousZoomLevel, newZoomLevel) {
        this._recursively(bounds, newZoomLevel, mapMinZoom,
            function (c) {
                c._recursivelyAnimateChildrenIn(bounds, c._group._map.latLngToLayerPoint(c.getLatLng()).round(), previousZoomLevel);

                //TODO: depthToAnimateIn affects _isSingleParent, if there is a multizoom we may/may not be.
                //As a hack we only do a animation free zoom on a single level zoom, if someone does multiple levels then we always animate
                if (c._isSingleParent() && previousZoomLevel - 1 === newZoomLevel) {
                    c.clusterShow();
                    c._recursivelyRemoveChildrenFromMap(bounds, mapMinZoom, previousZoomLevel); //Immediately remove our children as we are replacing them. TODO previousBounds not bounds
                } else {
                    c.clusterHide();
                }

                c._addToMap();
            }
        );
    },

    _recursivelyBecomeVisible: function (bounds, zoomLevel) {
        this._recursively(bounds, this._group._map.getMinZoom(), zoomLevel, null, function (c) {
            c.clusterShow();
        });
    },

    _recursivelyAddChildrenToMap: function (startPos, zoomLevel, bounds) {
        this._recursively(bounds, this._group._map.getMinZoom() - 1, zoomLevel,
            function (c) {
                if (zoomLevel === c._zoom) {
                    return;
                }

                //Add our child markers at startPos (so they can be animated out)
                for (var i = c._markers.length - 1; i >= 0; i--) {
                    var nm = c._markers[i];

                    if (!bounds.contains(nm._latlng)) {
                        continue;
                    }

                    if (startPos) {
                        nm._backupLatlng = nm.getLatLng();

                        nm.setLatLng(startPos);
                        if (nm.clusterHide) {
                            nm.clusterHide();
                        }
                    }

                    c._group._featureGroup.addLayer(nm);
                }
            },
            function (c) {
                c._addToMap(startPos);
            }
        );
    },

    _recursivelyRestoreChildPositions: function (zoomLevel) {
        //Fix positions of child markers
        for (var i = this._markers.length - 1; i >= 0; i--) {
            var nm = this._markers[i];
            if (nm._backupLatlng) {
                nm.setLatLng(nm._backupLatlng);
                delete nm._backupLatlng;
            }
        }

        if (zoomLevel - 1 === this._zoom) {
            //Reposition child clusters
            for (var j = this._childClusters.length - 1; j >= 0; j--) {
                this._childClusters[j]._restorePosition();
            }
        } else {
            for (var k = this._childClusters.length - 1; k >= 0; k--) {
                this._childClusters[k]._recursivelyRestoreChildPositions(zoomLevel);
            }
        }
    },

    _restorePosition: function () {
        if (this._backupLatlng) {
            this.setLatLng(this._backupLatlng);
            delete this._backupLatlng;
        }
    },

    //exceptBounds: If set, don't remove any markers/clusters in it
    _recursivelyRemoveChildrenFromMap: function (previousBounds, mapMinZoom, zoomLevel, exceptBounds) {
        var m, i;
        this._recursively(previousBounds, mapMinZoom - 1, zoomLevel - 1,
            function (c) {
                //Remove markers at every level
                for (i = c._markers.length - 1; i >= 0; i--) {
                    m = c._markers[i];
                    if (!exceptBounds || !exceptBounds.contains(m._latlng)) {
                        c._group._featureGroup.removeLayer(m);
                        if (m.clusterShow) {
                            m.clusterShow();
                        }
                    }
                }
            },
            function (c) {
                //Remove child clusters at just the bottom level
                for (i = c._childClusters.length - 1; i >= 0; i--) {
                    m = c._childClusters[i];
                    if (!exceptBounds || !exceptBounds.contains(m._latlng)) {
                        c._group._featureGroup.removeLayer(m);
                        if (m.clusterShow) {
                            m.clusterShow();
                        }
                    }
                }
            }
        );
    },

    //Run the given functions recursively to this and child clusters
    // boundsToApplyTo: a L.LatLngBounds representing the bounds of what clusters to recurse in to
    // zoomLevelToStart: zoom level to start running functions (inclusive)
    // zoomLevelToStop: zoom level to stop running functions (inclusive)
    // runAtEveryLevel: function that takes an L.MarkerCluster as an argument that should be applied on every level
    // runAtBottomLevel: function that takes an L.MarkerCluster as an argument that should be applied at only the bottom level
    _recursively: function (boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel) {
        var childClusters = this._childClusters,
            zoom = this._zoom,
            i, c;

        if (zoomLevelToStart <= zoom) {
            if (runAtEveryLevel) {
                runAtEveryLevel(this);
            }
            if (runAtBottomLevel && zoom === zoomLevelToStop) {
                runAtBottomLevel(this);
            }
        }

        if (zoom < zoomLevelToStart || zoom < zoomLevelToStop) {
            for (i = childClusters.length - 1; i >= 0; i--) {
                c = childClusters[i];
                if (c._boundsNeedUpdate) {
                    c._recalculateBounds();
                }
                if (boundsToApplyTo.intersects(c._bounds)) {
                    c._recursively(boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel);
                }
            }
        }
    },

    //Returns true if we are the parent of only one cluster and that cluster is the same as us
    _isSingleParent: function () {
        //Don't need to check this._markers as the rest won't work if there are any
        return this._childClusters.length > 0 && this._childClusters[0]._childCount === this._childCount;
    }
});

